package com.longcai.cm.base.filter;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.longcai.cm.moudules.system.domain.SysUsers;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * @项目名称：Ic_master
 * @类名称：KickoutSessionFilter
 * @类描述：自定义过滤器，进行用户访问控制
 * @创建人：chenyue
 * @创建时间：2018年11月22日 上午09:18:29 @version：
 */
public class KickoutSessionFilter extends AccessControlFilter {

    private static final Logger logger = LoggerFactory.getLogger(KickoutSessionFilter.class);

    private final static ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 踢出后到的地址
     */
    private String kickoutUrl;
    /**
     * 踢出之前登录的/之后登录的用户 默认false踢出之前登录的用户
     */
    private boolean kickoutAfter = false;
    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;

    private SessionManager sessionManager;

    private Cache<String, Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    /**
     * 设置Cache的key的前缀
     *
     * @param cacheManager
     */
    public void setCacheManager(CacheManager cacheManager) {
        // 必须和ehcache缓存配置中的缓存name一致
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
            throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String path = req.getRequestURI();
        logger.debug("===当前请求的uri：==" + path);
        String contextPath = req.getContextPath();
        logger.debug("===当前请求的域名或ip+端口：==" + contextPath);
        // 放行登录
        if (path.equals("/master/login")) {
            return true;
        }
        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            // 如果没有登录，直接进行之后的流程
            return true;
        }

        try {
            Session session = subject.getSession();
            SysUsers user = (SysUsers) subject.getPrincipal();
            String username = user.getUsername();
            logger.debug("===当前用户username：==" + username);
            Serializable sessionId = session.getId();
            logger.debug("===当前用户sessionId：==" + sessionId);

            // 读取缓存用户 没有就存入
            Deque<Serializable> deque = cache.get(username);
            logger.debug("===当前deque：==" + deque);
            // 如果此用户没有session队列，也就是还没有登录过，缓存中没有
            // 就new一个空队列，不然deque对象为空，会报空指针
            if (deque == null) {
                // 初始化队列
                deque = new LinkedList<Serializable>();
            }
            // 如果队列里没有此sessionId，且用户没有被踢出；放入队列
            if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
                // 将sessionId存入队列
                deque.push(sessionId);
                // 将用户的sessionId队列缓存
                cache.put(username, deque);
            }
            // 如果队列里的sessionId数超出最大会话数，开始踢人
            while (deque.size() > maxSession) {
                logger.debug("===deque队列长度：==" + deque.size());
                Serializable kickoutSessionId = null;
                // 是否踢出后来登录的，默认是false；即后者登录的用户踢出前者登录的用户；
                if (kickoutAfter) {
                    // 如果踢出后者
                    kickoutSessionId = deque.removeFirst();
                } else {
                    // 否则踢出前者
                    kickoutSessionId = deque.removeLast();
                }
                cache.put(username, deque);

                try {
                    // 获取被踢出的sessionId的session对象
                    Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                    if (kickoutSession != null) {
                        // 设置会话的kickout属性表示踢出了
                        kickoutSession.setAttribute("kickout", true);
                    }
                } catch (Exception e) {
                    logger.error("强制用户下线失败");
                }
            }

            // 如果被踢出了，(前者或后者)直接退出，重定向到踢出后的地址
            if ((Boolean) session.getAttribute("kickout") != null
                    && (Boolean) session.getAttribute("kickout") == true) {
                // 会话被踢出了
                try {
                    // 退出登录
                    subject.logout();
                } catch (Exception e) { // ignore
                }
                saveRequest(request);
                logger.debug("==踢出后用户重定向的路径kickoutUrl:" + kickoutUrl);
                return isAjaxResponse(request, response);
            }
            return true;
        } catch (Exception e) {
            logger.error("控制用户在线数量【education-->KickoutSessionFilter.onAccessDenied】异常！", e);
            return isAjaxResponse(request, response);
        }
    }

    private boolean isAjaxResponse(ServletRequest request, ServletResponse response) throws IOException {
        // ajax请求
        /**
         * 判断是否已经踢出 1.如果是Ajax 访问，那么给予json返回值提示。 2.如果是普通请求，直接跳转到登录页
         */
        Map<String, String> resultMap = new HashMap<String, String>();
        // 判断是不是Ajax请求
        if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
            logger.debug(getClass().getName() + "当前用户已经在其他地方登录，并且是Ajax请求！");
            resultMap.put("user_status", "002");
            // resultMap.put("user_status",
            // IStatusMessage.SystemStatus.MANY_LOGINS.getCode());
            resultMap.put("message", "您已在别处登录，请您修改密码或重新登录");
            // 输出json串
            out(response, resultMap);
        } else {
            // 重定向
//            WebUtils.issueRedirect(request, response, kickoutUrl);
            HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
            httpServletResponse.sendError(401, "登录信息已失效");
        }
        return false;
    }

    private void out(ServletResponse hresponse, Map<String, String> resultMap) throws IOException {
        try {
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.println(JSON.toJSONString(resultMap));
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("KickoutSessionFilter.class 输出JSON异常，可以忽略。");
        }
    }
}
